{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "raw_mimetype": "text/restructuredtext"
   },
   "source": [
    ".. _nb_custom:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Custom Variable Type\n",
    "\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In the following, we describe a custom variable problem. The variable is a string with a fixed length in our case. \n",
    "However, we formulate the problem to be easily extended to have a variable length.\n",
    "The objective function looks as follows:\n",
    "\n",
    "\\begin{align}\n",
    "\\begin{split}\n",
    "\\max f_1(x) & = & \\, \\# a \\\\[2mm]\n",
    "\\max f_2(x) & = & \\, \\# b \n",
    "\\end{split}\n",
    "\\end{align}\n",
    "\n",
    "The first objective is the number of a's in the string and the second the number of b's.\n",
    "For instance, for the variable \"abdfgdgabb\" the $f_1(x)=2$ and $f_2(x)=3$.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "from pymoo.core.problem import ElementwiseProblem\n",
    "\n",
    "class MyProblem(ElementwiseProblem):\n",
    "    \n",
    "    def __init__(self, n_characters=10):\n",
    "        super().__init__(n_var=1, n_obj=2, n_ieq_constr=0)\n",
    "        self.n_characters = n_characters\n",
    "        self.ALPHABET = [c for c in string.ascii_lowercase]\n",
    "\n",
    "    def _evaluate(self, x, out, *args, **kwargs):\n",
    "        n_a, n_b = 0, 0\n",
    "        for c in x[0]:\n",
    "            if c == 'a':\n",
    "                n_a += 1\n",
    "            elif c == 'b':\n",
    "                n_b += 1\n",
    "\n",
    "        out[\"F\"] = np.array([- n_a, - n_b], dtype=float)\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The problem definition above defines a problem with just one variable. This variable can be considered a complex object, which is, in our case, a string. The same principle can be used to use other data structures such as trees or lists with variable lengths. Because both objectives have to be maximized, we are minimizing their negative values."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To solve the optimization problem, evolutionary operators sampling, crossover, mutation, and duplication, check needs to be implemented.\n",
    "Each of the modules will be shown in the following."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Sampling\n",
    "\n",
    "Our sampling method just generates a random string, which is equivalent to choosing a random letter from the alphabet (only lower case).\n",
    "Because of the implementation of having only one variable, we return a matrix with the shape (n,1)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from pymoo.core.sampling import Sampling\n",
    "\n",
    "class MySampling(Sampling):\n",
    "\n",
    "    def _do(self, problem, n_samples, **kwargs):\n",
    "        X = np.full((n_samples, 1), None, dtype=object)\n",
    "\n",
    "        for i in range(n_samples):\n",
    "            X[i, 0] = \"\".join([np.random.choice(problem.ALPHABET) for _ in range(problem.n_characters)])\n",
    "\n",
    "        return X\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Crossover\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The crossover operator combines parents to create offsprings. In our framework, the crossover operator retrieves the input already with predefined matings. \n",
    "Our crossover randomly picks a character from the first or the second parent."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from pymoo.core.crossover import Crossover\n",
    "\n",
    "class MyCrossover(Crossover):\n",
    "    def __init__(self):\n",
    "        \n",
    "        # define the crossover: number of parents and number of offsprings\n",
    "        super().__init__(2, 2)\n",
    "\n",
    "    def _do(self, problem, X, **kwargs):\n",
    "        \n",
    "        # The input of has the following shape (n_parents, n_matings, n_var)\n",
    "        _, n_matings, n_var = X.shape\n",
    "        \n",
    "        # The output owith the shape (n_offsprings, n_matings, n_var)\n",
    "        # Because there the number of parents and offsprings are equal it keeps the shape of X\n",
    "        Y = np.full_like(X, None, dtype=object)\n",
    "\n",
    "        # for each mating provided\n",
    "        for k in range(n_matings):\n",
    "            \n",
    "            # get the first and the second parent\n",
    "            a, b = X[0, k, 0], X[1, k, 0]\n",
    "            \n",
    "            # prepare the offsprings\n",
    "            off_a = [\"_\"] * problem.n_characters\n",
    "            off_b = [\"_\"] * problem.n_characters\n",
    "            \n",
    "            for i in range(problem.n_characters):\n",
    "                if np.random.random() < 0.5:\n",
    "                    off_a[i] = a[i]\n",
    "                    off_b[i] = b[i]\n",
    "                else:\n",
    "                    off_a[i] = b[i]\n",
    "                    off_b[i] = a[i]\n",
    "\n",
    "            # join the character list and set the output\n",
    "            Y[0, k, 0], Y[1, k, 0] = \"\".join(off_a), \"\".join(off_b)\n",
    "            \n",
    "        return Y"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Mutation\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The mutation needs to be implemented for our string object as well. We either change the order of the string (40%), randomly pick a new character with a given probability (40%), or leave the string unmodified (20%). "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from pymoo.core.mutation import Mutation\n",
    "\n",
    "class MyMutation(Mutation):\n",
    "    def __init__(self):\n",
    "        super().__init__()\n",
    "\n",
    "    def _do(self, problem, X, **kwargs):\n",
    "        \n",
    "        # for each individual\n",
    "        for i in range(len(X)):\n",
    "            \n",
    "            r = np.random.random()\n",
    "            \n",
    "            # with a probabilty of 40% - change the order of characters\n",
    "            if r < 0.4:\n",
    "                perm = np.random.permutation(problem.n_characters)\n",
    "                X[i, 0] = \"\".join(np.array([e for e in X[i, 0]])[perm])\n",
    "                \n",
    "            # also with a probabilty of 40% - change a character randomly\n",
    "            elif r < 0.8:\n",
    "                prob = 1 / problem.n_characters\n",
    "                mut = [c if np.random.random() > prob \n",
    "                       else np.random.choice(problem.ALPHABET) for c in X[i, 0]]\n",
    "                X[i, 0] = \"\".join(mut)\n",
    "\n",
    "        return X"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Duplicates\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Moreover, do not underestimate the importance of filtering out duplicates during evolution. Duplicate elimination helps to make sure a mating produces an offspring that does not already exist in the current population. If it does, another mating process is triggered until unique individuals do exist."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from pymoo.core.duplicate import ElementwiseDuplicateElimination\n",
    "\n",
    "class MyDuplicateElimination(ElementwiseDuplicateElimination):\n",
    "\n",
    "    def is_equal(self, a, b):\n",
    "        return a.X[0] == b.X[0]\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Optimize\n",
    "\n",
    "Finally, we create an algorithm object with all the modules defined above."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import string\n",
    "import numpy as np\n",
    "\n",
    "from pymoo.algorithms.moo.nsga2 import NSGA2\n",
    "from pymoo.optimize import minimize\n",
    "\n",
    "\n",
    "algorithm = NSGA2(pop_size=11,\n",
    "                  sampling=MySampling(),\n",
    "                  crossover=MyCrossover(),\n",
    "                  mutation=MyMutation(),\n",
    "                  eliminate_duplicates=MyDuplicateElimination())\n",
    "\n",
    "res = minimize(MyProblem(),\n",
    "               algorithm,\n",
    "               ('n_gen', 100),\n",
    "               seed=1,\n",
    "               verbose=False)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from pymoo.visualization.scatter import Scatter\n",
    "Scatter().add(res.F).show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "results = res.X[np.argsort(res.F[:, 0])]\n",
    "count = [np.sum([e == \"a\" for e in r]) for r in results[:, 0]]\n",
    "print(np.column_stack([results, count]))"
   ]
  }
 ],
 "metadata": {
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
